package com.eshop.inventory.controller;

import com.eshop.inventory.VO.ResultVO;
import com.eshop.inventory.model.ProductInventory;
import com.eshop.inventory.request.ProductInventoryCacheRefreshRequest;
import com.eshop.inventory.request.ProductInventoryDBUpdateRequest;
import com.eshop.inventory.service.ProductInventoryService;
import com.eshop.inventory.service.RequestAsyncProcessService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

/**
 * @author: Xukai
 * @description:
 * @createDate: 2018/7/23 16:57
 * @modified By:
 */
@RestController
@Slf4j
public class ProductInventoryController {

    @Autowired
    private RequestAsyncProcessService requestAsyncProcessService;

    @Autowired
    private ProductInventoryService productInventoryService;

    /**
     * 更新商品库存
     */
    @PostMapping("/updateProductInventory")
    public ResultVO updateProductInventory(@Valid ProductInventory productInventory, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            log.error("【更新订单】参数不正确, productId={}", productInventory.getProductId());
            return ResultVO.failure();
        }
        log.info("商品库存更新请求：商品id={}, 商品库存数量={}", productInventory.getProductId(), productInventory.getInventoryCnt());
        try {
            ProductInventoryDBUpdateRequest dbUpdateRequest = new ProductInventoryDBUpdateRequest(productInventory, productInventoryService);
            requestAsyncProcessService.process(dbUpdateRequest);
            return ResultVO.success();
        } catch (Exception e) {
            e.printStackTrace();
            return ResultVO.failure();
        }
    }

    /**
     * 获取商品库存
     */
    @GetMapping("/getProductInventory")
    public ResultVO getProductInventory(@RequestParam("productId") Integer productId) {
        log.info("商品库存读请求：商品id={}", productId);
        // 这里做了读请求的去重优化，如果队列里已经有了一个写请求+一个读请求
        // 那就不再向队列里放读请求，因为放了也是重复无意义
        // 对于目前的读请求，只要hang一会，如果能在超时时间内等到前面的读请求异步执行完
        // 就能从缓存中读到数据，否则直接查数据库
        ProductInventoryCacheRefreshRequest cacheRefreshRequest = new ProductInventoryCacheRefreshRequest(productId, productInventoryService);
        requestAsyncProcessService.process(cacheRefreshRequest);
        ProductInventory productInventory;
        // 将请求扔给service异步去处理以后，就需要while(true)一会儿，在这里hang住
        // 去尝试等待前面有商品库存更新的操作，同时缓存刷新的操作，将最新的数据刷新到缓存中
        long startTime = System.currentTimeMillis();
        long endTime;
        long waitTime = 0L;
        while (true) {
            // 一般公司面向用户的读请求控制在200ms
            if (waitTime > 200) {
                break;
            }
            // 尝试去redis中读取一次商品库存的缓存数据
            productInventory = productInventoryService.getProductInventoryCache(productId);
            if (productInventory != null) {
                // 如果读取到结果就返回
                log.info("在200ms内读取到了redis中的库存缓存，商品id={},商品库存数量={}", productId, productInventory.getInventoryCnt());
                return ResultVO.success(productInventory);
            } else {
                // 如果没有读取到结果，那么等待一段时间(20ms)
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                endTime = System.currentTimeMillis();
                waitTime = endTime - startTime;
            }
        }
        // 代码会运行到这里，只有三种情况：
        // 1、上一次也是读请求，数据刷入了redis，但是redis LRU算法给清理掉了，标志位还是false
        // 所以不仅读缓存操作从缓存中拿不到数据，而且还无法执行队列中读请求的刷新缓存功能，因为标志位是false
        // 2、可能在200ms内，就是读请求在队列中一直积压着，没有等待到它执行（在实际生产环境中，基本是比较坑了）
        // 所以就直接查一次库，教程中给的解决方法是向队列里塞进去一个刷新缓存的请求，加一个强制刷新标志
        // 我认为是有问题的，因为前面的读请求都来不及执行，再加一个读请求又有什么用呢
        // 3、数据库本身就没有数据，所以缓存一直都没有数据，穿透redis，请求到达mysql库
        productInventory = productInventoryService.findProductInventory(productId);
        if (productInventory != null) {
            // 将缓存刷新一下
            // 一个读请求过来，将数据库中的数刷新到了缓存中，flag是false，然后过了一会儿，redis内存满了，自动删除了这个额缓存
            // 下一次读请求再过来，发现flag是false，就不会去执行刷新缓存的操作了
            // 而是hang在哪里，反复循环，等一会儿，发现在缓存中始终查询不到数据，然后就去数据库里查询，就直接返回了
            // 这种代码，就有可能会导致，缓存永远变成null的情况
            // 解决方法一：在controller中，如果在数据库中查询到了，直接刷新到缓存里面去，以后的读请求就又可以从缓存里面读了
            // 解决方法二：在controller中，如果在数据库中查询到了，就修改内存队列的flagMap对应的productId的boolean状态，改成true
            // 方法一是有问题的，因为本架构就是要解决缓存在并发状态下数据不一致的问题
            // 方法一还用直接刷缓存的方法更新缓存，在并发状态下就有数据不一致的风险
            // 修改flag要比直接更新redis靠谱，就算有延迟等问题，只不过修改了flag而已，对数据毫无影响
            // 另外修改flag的好处在于，就算队列里有积压的读请求，一旦将flag改成true，排在读请求第一位的轮到了就可以执行刷新缓存
            // 比教程里说的添加一个强刷标志提高了一点点效率，毕竟强刷也得轮到了才能强刷，而前面还等了好多读请求呢
            try {
                requestAsyncProcessService.setFlag(productId, true);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return ResultVO.success(productInventory);
        }
        return ResultVO.failure();
    }

}
